import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:flutter/services.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:synchronized/synchronized.dart';

import 'ble_device_filter.dart';
import 'ble_uuid.dart';

class BlePlugin {
  static const MethodChannel _channel = const MethodChannel('ble_plugin');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

typedef void OnNotified(ActiveDevice device, List<int> values);
typedef void OnStateChanged(
    BluetoothDevice device, BluetoothConnectionState state,
    {String? flag});

class BleManager {
  BleManager._privateConstructor() {
    FlutterBluePlus.adapterState.listen((state) {
      if (state == BluetoothAdapterState.off) {
        // 系统状态变为off
        if (Platform.isIOS) {
          // iOS系统蓝牙off后，不会回调disconnect事件
          print(
              "phone bt state changed to off, prepare to trigger disconnect event manually");
          connectingPool.keys.forEach((id) {
            print("trigger [$id] disconnected");
            _handleDisconnectEvent(BluetoothDevice.fromId(id), null);
          });
        }
      }
    });
  }

  static final BleManager _instance = BleManager._privateConstructor();

  factory BleManager() {
    return _instance;
  }

  List<BleUuid> supportedUuidList = [];

  BleDeviceFilter bleDeviceScanFilter = BleDeviceFilter.defaultFilter;

  Map<String, StreamSubscription<BluetoothConnectionState>> connectingPool = {};

  Map<String, ActiveDevice> activeDevices = Map();

  /// 蓝牙状态flag map
  /// identifier -> {state -> flag}
  Map<String, Map<BluetoothConnectionState, String>> btStateFlagMap = Map();

  OnNotified? onNotifiedCallback;
  OnStateChanged? onStateChangedCallback;

  BluetoothAdapterState btState = BluetoothAdapterState.unknown;

  String? nameToConnect;
  String? idToConnect;
  BluetoothDevice? connectingDevice;

  // 搜索次数统计
  ScanStat scanStat = ScanStat();

  Function? supportRequestMtuFunc;
  Function? supportedMtuFunc;

  /*
     * <p>
     * 无可用 BLE service 或 characteristic 时的 handler
     * </p>
     *
     * @param deviceIdentifier String
     *
     * @param deviceName String </br>
     *
     * return bool
     * TRUE - 已处理，不需要断开
     * FALSE - 未处理，断开连接
     */
  Function? noValidBleServiceHandler;

  Future<BluetoothAdapterState> getBtState() {
    return FlutterBluePlus.adapterState.first;
  }

  ActiveDevice? getActiveDevice(String id) {
    return activeDevices[id];
  }

  void addSupportedBleService(BleUuid uuidInfo) {
    supportedUuidList.add(uuidInfo);
  }

  void addSupportedBleServices(List<BleUuid> uuidList) {
    supportedUuidList.addAll(uuidList);
  }

  Stream<ScanResult> scan(
      {BleDeviceFilter? filter,
      List<Guid> services = const [],
      List<String> names = const [],
      List<String> remoteIds = const [],
      Duration? timeout,
      bool interruptLastIfExist = true}) {
    if (timeout == null) timeout = const Duration(seconds: 10);

    StreamController<ScanResult> streamController = StreamController();
    if (!scanStat.tryStartScan()) {
      throw ScanTooFrequentlyException(scanStat.getMillisecondsNeedWaiting());
    }

    // listen to scan results
    // Note: `onScanResults` only returns live scan results, i.e. during scanning. Use
    //  `scanResults` if you want live scan results *or* the results from a previous scan.

    // 通过过滤器过滤设备
    void filterDevice(ScanResult r) {
      if (filter != null) {
        if (filter.accept(r) == true) {
          streamController.add(r);
        }
      } else {
        streamController.add(r);
      }
    }

    var subscription = FlutterBluePlus.onScanResults.listen(
      (results) async {
        await Future.delayed(Duration(milliseconds: Random().nextInt(20) + 5));
        if (results.isNotEmpty && results.last.device.advName.isNotEmpty) {
          ScanResult r = results.last; // the most recently found device
          if (names.isNotEmpty) {
            if (names.contains(r.device.advName)) {
              filterDevice(r);
            }
          } else {
            filterDevice(r);
          }
        }
      },
      onError: (e) => streamController.addError(e),
    );

    // cleanup: cancel subscription when scanning stops
    FlutterBluePlus.cancelWhenScanComplete(subscription);

    // Wait for Bluetooth enabled & permission granted
    // In your real app you should use `FlutterBluePlus.adapterState.listen` to handle all states
    FlutterBluePlus.adapterState.listen((event) {
      if (event == BluetoothAdapterState.on) {
        FlutterBluePlus.startScan(
          androidScanMode: AndroidScanMode.balanced,
          withServices: services,
          withRemoteIds: remoteIds,
          timeout: timeout,
          continuousDivisor: Platform.isAndroid ? 8 : 1,
        );
      }
    });

    return streamController.stream;
  }

  Future<dynamic> startScan({Duration? timeout}) {
    return FlutterBluePlus.startScan(timeout: timeout);
  }

  Future<void> stopScan() {
    return FlutterBluePlus.stopScan();
  }

  List<StreamSubscription> dataNotifiedSubscriptions = [];

  void _cancelAllDataNotifiedSubscriptions() {
    for (var subscription in dataNotifiedSubscriptions) {
      subscription.cancel();
    }
  }

  void connect(
      {BluetoothDevice? device,
      String? name,
      String? id,
      Duration? timeout,
      Function? timeoutCallback,
      String? flag,
      Duration? scanTimeout}) {
    if (device != null) {
      _connectByDevice(device,
          flag: flag, timeout: timeout, timeoutCallback: timeoutCallback);
      return;
    }

    if (name != null || id != null) {
      if (scanTimeout == null) scanTimeout = Duration(seconds: 5);
      _connectByNameId(name, id,
          timeout: timeout,
          scanTimeout: scanTimeout,
          timeoutCallback: timeoutCallback,
          flag: flag);
      return;
    }
    throw Exception(
        'An error occurred when connect.[Unknown target device to connect.]');
  }

  void _connectByDevice(BluetoothDevice device,
      {Duration? timeout, Function? timeoutCallback, String? flag}) {
    device.connect(
        autoConnect: false, timeout: timeout ?? Duration(seconds: 5));

    connectingDevice = device;
    handleBtStateFlagIfNeed(
        device.remoteId.str, BluetoothConnectionState.connected, flag);
    // listen ble connection state change event

    StreamSubscription<BluetoothConnectionState> stateChangeStream =
        device.connectionState.skip(1).listen((state) {
      print("device [" +
          device.advName +
          "] state changed to " +
          state.toString());
      switch (state) {
        case BluetoothConnectionState.connected:
          device.discoverServices().then((services) async {
            // listen ble services discovery event
            String printInfo = "";
            for (BluetoothService service in services) {
              printInfo += service.uuid.toString();
              printInfo += ", characteristic: ";
              List<BluetoothCharacteristic> characteristics =
                  service.characteristics;
              for (BluetoothCharacteristic characteristic in characteristics) {
                printInfo += characteristic.uuid.toString() + ", ";
              }
              printInfo += "\n";
            }
            print("services: " + "\n" + printInfo);
            if (services.isEmpty) {
              print("device [" + device.advName + "] has no services.");
              device.disconnect();
              return;
            }
            ActiveDevice? activeDevice = retrieveActiveDevice(device, services);
            if (activeDevice == null) {
              print("can not retrieve active device from [" +
                  device.advName +
                  "].");
              bool shouldDisconnect = true;
              if (noValidBleServiceHandler != null) {
                try {
                  if (noValidBleServiceHandler!(
                      device.remoteId.str, device.advName)) {
                    shouldDisconnect = false;
                  }
                } catch (e) {
                  print(
                      "Error occurred when execute 'noValidBleServiceHandler', $e");
                }
              }
              if (shouldDisconnect) device.disconnect();
              return;
            }
            connectingDevice = null;
            await activeDevice.notifyCharacteristic.setNotifyValue(true);

            if (shouldRequestMtu()) {
              activeDevice.fixedMtu =
                  await activeDevice.device.requestMtu(supportedMtuFunc!() + 5);
            } else {
              if (supportedMtuFunc != null)
                activeDevice.fixedMtu = supportedMtuFunc!();
            }

            activeDevices[device.remoteId.str] = activeDevice;

            _cancelAllDataNotifiedSubscriptions();

            activeDevice.onDataNotifiedSubscription = activeDevice
                .notifyCharacteristic.lastValueStream
                .listen((values) {
              if (onNotifiedCallback != null)
                onNotifiedCallback!(activeDevice, values);
            });

            dataNotifiedSubscriptions
                .add(activeDevice.onDataNotifiedSubscription!);

            if (onStateChangedCallback != null)
              onStateChangedCallback!(
                  device, BluetoothConnectionState.connected,
                  flag: getBtStateFlag(
                      device.remoteId.str, BluetoothConnectionState.connected));
          });
          break;
        case BluetoothConnectionState.disconnected:
          _handleDisconnectEvent(device, flag);
          break;
        default:
          if (onStateChangedCallback != null)
            onStateChangedCallback!(device, state,
                flag: getBtStateFlag(device.remoteId.str, state));
          break;
      }
    });

    connectingPool[device.remoteId.str] = stateChangeStream;
  }

  void _connectByNameId(String? name, String? id,
      {Duration? timeout,
      Duration scanTimeout = const Duration(seconds: 5),
      Function? timeoutCallback,
      String? flag}) {
    nameToConnect = name;
    idToConnect = id;
    List<String> remoteIds = [];
    if (id != null) remoteIds.add(id);
    scan(timeout: scanTimeout, remoteIds: remoteIds).listen((scanResult) async {
      if (scanResult.device.advName == nameToConnect ||
          scanResult.device.remoteId.str == id) {
        await stopScan();

        connect(
            device: scanResult.device,
            timeout: timeout,
            timeoutCallback: timeoutCallback,
            flag: flag);

        // reset
        nameToConnect = null;
        idToConnect = null;
      }
    });

    /// 主动扫描超时检查
    Future.delayed(scanTimeout, () {
      if (nameToConnect == null && idToConnect == null) {
        /// 已扫描到指定设备，并开始连接
        return;
      }

      /// 扫描超时，未扫描到指定设备
      if (timeoutCallback != null) timeoutCallback();
    });
  }

  Future<int> requestMtu(int mtu,
      {BluetoothDevice? device, String? identifier}) {
    if (device != null) {
      return device.requestMtu(mtu);
    }
    if (identifier != null) {
      if (!activeDevices.containsKey(identifier)) {
        throw Exception('Unknown identifier [$identifier] to request mtu!');
      }
      return activeDevices[identifier]!.device.requestMtu(mtu);
    }
    throw Exception('Unknown device to request mtu!');
  }

  bool shouldRequestMtu() {
    return Platform.isAndroid &&
        supportRequestMtuFunc != null &&
        supportedMtuFunc != null &&
        supportRequestMtuFunc!();
  }

  Future disconnect(
      {BluetoothDevice? device, String? identifier, String? flag}) {
    if (device != null) {
      if (idToConnect != null && device.remoteId.str == idToConnect) {
        idToConnect = null;
      }
      if (nameToConnect != null && device.advName == nameToConnect) {
        nameToConnect = null;
      }
      if (connectingDevice != null &&
          connectingDevice!.remoteId.str == device.remoteId.str) {
        connectingDevice = null;
      }
      handleBtStateFlagIfNeed(
          device.remoteId.str, BluetoothConnectionState.disconnected, flag);
      print(DateTime.now().toString() + " disconnect by device");
      return device.disconnect();
    }

    if (identifier != null) {
      print(DateTime.now().toString() + " disconnect by device id");
      if (idToConnect == identifier) {
        idToConnect = null;
        print(DateTime.now().toString() +
            " target device id is not scanned, reset idToConnect");
        return Future.value();
      }

      if (activeDevices[identifier] != null) {
        handleBtStateFlagIfNeed(
            identifier, BluetoothConnectionState.disconnected, flag);
        if (connectingDevice != null &&
            activeDevices[identifier]!.device.remoteId.str ==
                connectingDevice!.remoteId.str) {
          connectingDevice = null;
        }
        print(DateTime.now().toString() +
            " target device is connected, disconnect by active device");
        return activeDevices[identifier]!.device.disconnect();
      }

      if (connectingDevice != null &&
          connectingDevice!.remoteId.str == identifier) {
        handleBtStateFlagIfNeed(
            identifier, BluetoothConnectionState.disconnected, flag);
        print(DateTime.now().toString() +
            " target device is connecting, disconnect by device");
        var r = connectingDevice!.disconnect();
        connectingDevice = null;
        return r;
      }
    }

    if (activeDevices.values.isEmpty)
      return Future.error('There is no activated or connecting device.');
    handleBtStateFlagIfNeed(activeDevices.values.first.device.remoteId.str,
        BluetoothConnectionState.disconnected, flag);
    if (connectingDevice != null &&
        activeDevices.values.first.device.remoteId.str ==
            connectingDevice!.remoteId.str) {
      connectingDevice = null;
    }
    return activeDevices.values.first.device.disconnect();
  }

  void _handleDisconnectEvent(BluetoothDevice device, flag) {
    connectingDevice = null;
    // 手动重连避免某些情况下的自动重连
    device.disconnect();
    // remove the disconnected device from active-device map
    String deviceId = device.remoteId.str;

    connectingPool[deviceId]?.cancel();
    connectingPool.remove(deviceId);

    activeDevices[deviceId]?.onDataNotifiedSubscription?.cancel();
    activeDevices[deviceId]?.onStateChangedSubscription?.cancel();

    _cancelAllDataNotifiedSubscriptions();

    activeDevices.remove(device.remoteId.str);
    if (onStateChangedCallback != null)
      onStateChangedCallback!(device, BluetoothConnectionState.disconnected,
          flag: getBtStateFlag(
              device.remoteId.str, BluetoothConnectionState.disconnected));
    handleBtStateFlagIfNeed(
        device.remoteId.str, BluetoothConnectionState.disconnected, flag);
  }

  void handleBtStateFlagIfNeed(
      String identifier, BluetoothConnectionState state, String? flag) {
    if (!btStateFlagMap.containsKey(identifier)) {
      if (flag == null) return;
      btStateFlagMap[identifier] = Map<BluetoothConnectionState, String>();
    }
    if (flag == null) {
      btStateFlagMap[identifier]!.remove(state);
    } else {
      btStateFlagMap[identifier]![state] = flag;
    }
  }

  String? getBtStateFlag(String identifier, BluetoothConnectionState state,
      {bool removeAfterGet = true}) {
    if (!btStateFlagMap.containsKey(identifier)) return null;
    String? flag = btStateFlagMap[identifier]![state];
    if (flag != null && removeAfterGet) {
      btStateFlagMap[identifier]!.remove(state);
    }
    return flag;
  }

  Future write(List<int> values, {String? identifier}) async {
    if (identifier != null && activeDevices[identifier] != null) {
      return _write0(
          activeDevices[identifier]!.writeCharacteristic,
          values,
          activeDevices[identifier]!.fixedMtu,
          activeDevices.values.first.writeWithoutResponse);
    }
    if (activeDevices.values.length > 0) {
      return _write0(
          activeDevices.values.first.writeCharacteristic,
          values,
          activeDevices.values.first.fixedMtu,
          activeDevices.values.first.writeWithoutResponse);
    }
    print(
        "ERROR! An error occurred when write BLE data. [There is no active device to write.]");
  }

  final writeLock = Lock();

  Future _write0(BluetoothCharacteristic characteristic, List<int> values,
      int sizePerPkg, bool writeWithoutResponse) async {
    StringBuffer hex = StringBuffer();
    for (int i in values)
      hex.write(i < 16 ? '0' + i.toRadixString(16) : i.toRadixString(16));
    print("write characteristic " +
        (writeWithoutResponse ? 'without response' : 'with response') +
        ", values: " +
        hex.toString());
    int total = values.length, sentSize = 0, remain = total;
    if (total <= sizePerPkg)
      return _writePkgByPkg(characteristic, values, writeWithoutResponse);
    writeLock.synchronized(() async {
      List<int> buffer;
      while (remain > 0) {
        buffer = values.sublist(
            sentSize, sentSize + (remain > sizePerPkg ? sizePerPkg : remain));
        await _writePkgByPkg(characteristic, buffer, writeWithoutResponse);
        await Future.delayed(const Duration(milliseconds: 20));
        sentSize += buffer.length;
        remain = total - sentSize;
      }
    });
  }

  Future _writePkgByPkg(BluetoothCharacteristic characteristic,
      List<int> values, bool writeWithoutResponse) {
    print("_writePkgByPkg, size: ${values.length}");
    return characteristic.write(values, withoutResponse: writeWithoutResponse);
  }

  ActiveDevice? retrieveActiveDevice(
      BluetoothDevice device, List<BluetoothService> services) {
    BluetoothService? writeService;
    BluetoothCharacteristic? writeCharacteristic;
    BluetoothService? notifyService;
    BluetoothCharacteristic? notifyCharacteristic;
    for (BleUuid supportedUuid in supportedUuidList) {
      for (BluetoothService service in services) {
        List<BluetoothCharacteristic> characteristics = service.characteristics;
        if (service.uuid.toString() == supportedUuid.notifyServiceUuid) {
          for (BluetoothCharacteristic characteristic in characteristics) {
            if (characteristic.uuid.toString() !=
                supportedUuid.notifyCharacteristicUuid) continue;
            notifyService = service;
            notifyCharacteristic = characteristic;
            break;
          }
          for (BluetoothCharacteristic characteristic in characteristics) {
            if (characteristic.uuid.toString() !=
                supportedUuid.writeCharacteristicUuid) continue;
            writeService = service;
            writeCharacteristic = characteristic;
            break;
          }
        } else if (service.uuid.toString() == supportedUuid.writeServiceUuid) {
          for (BluetoothCharacteristic characteristic in characteristics) {
            if (characteristic.uuid.toString() !=
                supportedUuid.writeCharacteristicUuid) continue;
            writeService = service;
            writeCharacteristic = characteristic;
            break;
          }
          for (BluetoothCharacteristic characteristic in characteristics) {
            if (characteristic.uuid.toString() !=
                supportedUuid.notifyCharacteristicUuid) continue;
            notifyService = service;
            notifyCharacteristic = characteristic;
            break;
          }
        }
      }
      if (writeService != null &&
          writeCharacteristic != null &&
          notifyService != null &&
          notifyCharacteristic != null) break;
    }
    if (writeService == null || notifyService == null) {
      return null;
    }
    return ActiveDevice(device, writeService, writeCharacteristic!,
        notifyService, notifyCharacteristic!);
  }

  bool config(Map<String, Object> config, {String? id}) {
    if (activeDevices.isEmpty) return false;
    ActiveDevice? device;
    if (id == null) {
      device = activeDevices.values.first;
    } else {
      device = getActiveDevice(id);
    }
    if (device == null) return false;

    if (config.containsKey('writeWithoutResponse')) {
      device.writeWithoutResponse = config['writeWithoutResponse'] as bool;
    }
    if (config.containsKey('mtu')) {
      device.fixedMtu = config['mtu'] as int;
    }
    return true;
  }
}

class ActiveDevice {
  BluetoothDevice device;
  BluetoothService writeService;
  BluetoothCharacteristic writeCharacteristic;
  BluetoothService notifyService;
  BluetoothCharacteristic notifyCharacteristic;

  BluetoothConnectionState state = BluetoothConnectionState.disconnected;
  Stream<BluetoothConnectionState>? stream;

  StreamSubscription? onStateChangedSubscription;
  StreamSubscription? onDataNotifiedSubscription;

  int fixedMtu = 20;
  bool writeWithoutResponse = false;

  ActiveDevice(this.device, this.writeService, this.writeCharacteristic,
      this.notifyService, this.notifyCharacteristic);
}

class Util {
  static bool isValidMac(String? mac) {
    return true;
  }
}

/*
 * 搜索次数统计
 */
class ScanStat {
  int _scanTimes = 0;
  int firstScanTimestamp = 0;

  bool tryStartScan() {
    if (!Platform.isAndroid) return true;
    _scanTimes++;
    if (_scanTimes == 1) {
      firstScanTimestamp = DateTime.now().millisecondsSinceEpoch;
      Future.delayed(const Duration(seconds: 30), () {
        _scanTimes = 0;
      });
    }
    return _scanTimes <= 5;
  }

  int getMillisecondsNeedWaiting() {
    return 30 * 1000 -
        DateTime.now().millisecondsSinceEpoch -
        firstScanTimestamp;
  }
}

class ScanTooFrequentlyException extends Error {
  int millisecondsLeft = 0;

  ScanTooFrequentlyException(this.millisecondsLeft);
}
